package task3.tracking;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class TrackingService extends Service implements LocationListener{
	
	private LocationManager locationManager;
	
	private int updateCount =0;
	private int requestCount = 0;
	DatagramSocket clientSocket;
	private double longitude;
	private double latitude;
	private boolean isFirstTime;
	private boolean kill = false;
	private Location currentLocation;
	private Location lastLocationFromServer;
	private double[] predictionVector;
	private boolean firstTimeUPD;
	private TypeConverter converter;
	
	private final String info ="INFO";
	private final String error="ERROR";
	private double mobileID = 1;
	
	private long sensingTime = 1; // Seconds
	private long maxSpeed = 5; // Meters / second
	private long updateThreshold = 20; // Meters
	
	private File sdCard = Environment.getExternalStorageDirectory();
	private final SimpleDateFormat pointDateFormatter = new SimpleDateFormat("yyyy-MM-dd 'T' HH:mm:ss'Z'");
	private final String xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>";
	private final String gpxTag = "<gpx"
	        + " xmlns=\"http://www.topografix.com/GPX/1/1\""
	        + " version=\"1.1\""
	        + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
	        + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd \">";
	private final String metaDataTag = "<metadata>"+
		"\n<time>"+pointDateFormatter.format(new Date())+"</time>"+
		"\n<bounds minlat=\"48.469001667\" minlon=\"9.000000000\" maxlat=\"49.000000000\" maxlon=\"9.999900000\"/>"+
		"\n</metadata> ";
	
	
	public void onCreate(){
		super.onCreate();
		isFirstTime = true;
		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); 
		locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, sensingTime*1000, 0, this);
		try {
			clientSocket= new DatagramSocket(6001);
		} catch (SocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		firstTimeUPD = true;
		
	}
	
	public void onDestroy(){
		kill = true;
		transmitPositionInformationViaUPD(latitude, longitude, mobileID, 0, 0, 0, 0);
		sdCard = Environment.getExternalStorageDirectory();
		if(sdCard.canWrite()){
			Log.i(info, "Can write to sdcard");
			File gpxFile = new File(sdCard,"gpxfile.gpx");
			
			try {
				FileWriter gpxWriter = new FileWriter(gpxFile,true);
				BufferedWriter out = new BufferedWriter(gpxWriter);
				
				out.write("</trkseg>"+"\n");
				out.write("</trk>"+"\n");
				out.write("</gpx>");
				
				out.close();
			} 
			catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		super.onDestroy();
	}
	
	public double getTime(){
		return System.currentTimeMillis();
	}
	private void onStop(){
		//sendKillSignalToServer();
	
	}
	
	private void updateGpxFile(Location location){
		
		if(sdCard.canWrite()){
		
			try {
				
				File gpxFile = new File(sdCard,"gpxfile.gpx");
				
				if(!gpxFile.exists()) {
					gpxFile.createNewFile();
				}
				
				FileWriter gpxWriter = new FileWriter(gpxFile,true);
				
				// If this method is called for the first time during the life cycle of the service
				// overwrite the already existing file. Write the GPX start tags.
				
				if(isFirstTime){
					gpxWriter = new FileWriter(gpxFile);
					BufferedWriter bfw = new BufferedWriter(gpxWriter);
					bfw.write(xmlHeader+"\n");
					bfw.write(gpxTag+"\n");
					bfw.write(metaDataTag+"\n");
					bfw.write("<trk>"+"\n");
					bfw.write("<trkseg>"+"\n");
					bfw.close();
					isFirstTime=false;
				}
				
				// If not, simply append to the already existing file
				else{
					gpxWriter = new FileWriter(gpxFile,true);
				}
				
				BufferedWriter out = new BufferedWriter(gpxWriter);
				
				String trackPointTag="<trkpt lat=\""+location.getLatitude()+"\" lon=\""+location.getLongitude()+"\">"+
				"    \n<time>"+getTime()+"</time>\n"+
				"</trkpt>\n";
				
				out.write(trackPointTag+"\n");
			
				out.close();
			}
			catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	private void linearDeadReckoning(Location estimatedLocation){
		Log.i(info, "Linear Dear Reckoning");
		if(currentLocation.distanceTo(lastLocationFromServer) + maxSpeed*sensingTime < updateThreshold ){
			return;
		}
		else{
			float azimuth = currentLocation.getBearing();
			Log.i(info, "actual bearing"+azimuth);
			float x, y;
			float distanceLength = (float) maxSpeed*sensingTime;
			int radius = 6381000;
			
			//4 possibilities are considered: 
			//someone is walking in north-east,north or east  direction
			if(azimuth >= 0 && azimuth <=90  ) {
			x =(float) (distanceLength * Math.sin(azimuth)); 
			y = (float) (distanceLength*Math.cos(azimuth));
			predictionVector[0]= Math.toDegrees(2*Math. asin(x/(2*radius)));
			predictionVector[1]= Math.toDegrees(2*Math. asin(y/(2*radius)));

			}
			//south-east or south direction
			else if(azimuth > 90 && azimuth <=180)  {
				x =Math.abs((float) (distanceLength * Math.sin(Math.toDegrees(180 - azimuth)))); 
				y = Math.abs((float) (distanceLength * Math.cos(Math.toDegrees(180 - azimuth))));
				predictionVector[0]= Math.toDegrees(2*Math. asin(x/(2*radius)));
				predictionVector[1]= Math.toDegrees(2*Math. asin(y/(2*radius)))*(-1);
			}
			//south-west or west 
			else if(azimuth > 180 && azimuth <=270){
	
					x =Math.abs((float) (distanceLength * Math.cos(Math.toDegrees(270 - azimuth)))); 
					y = Math.abs((float) (distanceLength * Math.sin(Math.toDegrees(270 - azimuth))));
					predictionVector[0]= Math.toDegrees(2*Math. asin(x/(2*radius)))*(-1);
					predictionVector[1]= Math.toDegrees(2*Math. asin(y/(2*radius)))*(-1);
			}
			//north-west
			else {
				x = Math.abs((float) (distanceLength * Math.sin(Math.toDegrees(360 - azimuth))));  
				y = Math.abs((float) (distanceLength * Math.cos(Math.toDegrees(360 - azimuth))));
				predictionVector[0]= Math.toDegrees(2*Math. asin(x/(2*radius)))*(-1);
				predictionVector[1]= Math.toDegrees(2*Math. asin(y/(2*radius)));
			
			}
			
			//TODO: Change prediction vector, send to server.
			transmitPositionInformationViaUPD(latitude,longitude,mobileID,System.currentTimeMillis(),predictionVector[0],predictionVector[1],maxSpeed);
			
		}
	}
	
//	private void sendKillSignalToServer(){
//		 
// 	  Socket clientSocket;
// 	  	try {
// 	  		clientSocket = new Socket("10.0.2.2", 8000);
// 	  		
// 	  			PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true);
// 	
// 	  			out.write("KILL "+ mobileID);
// 	  			
// 	  			out.close();
// 	  			clientSocket.close();
// 	  			
// 	  	} catch (UnknownHostException e) {
// 		  e.printStackTrace();
// 	  	} catch (IOException e) {
// 		  e.printStackTrace();
// 	  	}
// 	  	
//	}
	
	public final service.Stub iBinder = new service.Stub(){

		public double[] updateLocation() throws RemoteException {
			currentLocation = locationManager.getLastKnownLocation(locationManager.GPS_PROVIDER);
			onLocationChanged(currentLocation);

			if(lastLocationFromServer == null){
				double[] position = new double[2];
				position[0] = 0; // LATITUDE!!
				position[1] = 1; // LONGITUDE!!
				position = getPositionQueryToServerViaTCP();
				return position;
			}
			else {
				double[] position = new double[2];
				position[0] = lastLocationFromServer.getLatitude(); // LATITUDE!!
				position[1] = lastLocationFromServer.getLongitude(); // LONGITUDE!!
			
				return position;
			}
		}

		public void sendInitialPosToServer() throws RemoteException {
			
		}

		@Override
		public float getReductionRatio() throws RemoteException {
			return (float) updateCount/requestCount;
		}
		
	};

    /**
     * Function which receives location information from Server using TCP protocol 
     */
    private double[] getPositionQueryToServerViaTCP(){
 	 Log.i(info, "Position Query via TCP");
    	double[] updatedLocation = new double[2] ;
 		  //initializaiton of a socket
 	  	try {
 	  		Socket clientSocket = new Socket("10.0.2.2", 8000);
 	  	// Bidirectional connection established
 	  		if(clientSocket.isBound()){
 	  		InputStream is = clientSocket.getInputStream();
 	  		OutputStream os = clientSocket.getOutputStream();
 	  		byte[] data = new byte[256];
 	  		Log.i(info, "Tcp from user "+mobileID);
 	  		data = converter.toByta(mobileID);
 	  		os.write(data); // send some data
 	  		os.flush();
 	  		data = new byte[256];
 	  		
 	  		is.read(data); // receive some data
 	  		// close socket
 	  		is.close();
 	  		os.close();
 	  		clientSocket.close();

 	  			double [] receivedArray = new double[2];
 	  			        receivedArray = converter.toDoubleA(data);
 	  			        
 	  			updatedLocation[0] = receivedArray[0]; 
 	  			updatedLocation[1] = receivedArray[1];
 	  			Log.i(info, "Received from server:"+updatedLocation[0]+","+updatedLocation[1]);
 	  			requestCount++;
 	  		}
 	  		else{
 	  			Log.i(error, "TCP client socket unbound");
 	  		}
 	  			
 	  	} catch (UnknownHostException e) {
 		  e.printStackTrace();
 	  	} catch (IOException e) {
 		  e.printStackTrace();
 	  	}
 	  	
 	  	return updatedLocation;
   }
	
	public void onLocationChanged(Location loc) {

		Log.i("INFO", "Location has changed");
		longitude = loc.getLongitude();
		latitude = loc.getLatitude();
		double time = getTime();
		currentLocation = loc;
		boolean first = true;
		if(!isFirstTime)first=false;
		if(isFirstTime){
			// Send position and initial vector (0,0) to server.
			Log.i(info, "Initial location is beeing sent: Lat"+latitude+" Long"+longitude+" Time:" +time);
			predictionVector = new double[2];
			predictionVector[0] =0;
			predictionVector[1] =0;
			transmitPositionInformationViaUPD(latitude,longitude,mobileID,time,predictionVector[0],predictionVector[1],maxSpeed);
			first = false;
		
		}
		
		updateGpxFile(currentLocation);

		if(!first){

			double[] position = new double[2];
			position[0] = 0; // LATITUDE!!
			position[1] = 1; // LONGITUDE!!
		
			
			
			
			
			position = getPositionQueryToServerViaTCP();
			
			Location newLoc = new Location("From Server");
			newLoc.setLatitude(position[0]);
			newLoc.setLongitude(position[1]);

			lastLocationFromServer = newLoc;
			linearDeadReckoning(lastLocationFromServer);

		}
		
//		long endTime = System.currentTimeMillis();
//		long startTime = endTime;
//		while(startTime-endTime<1000){
//			startTime = System.currentTimeMillis();
//		}

		
		
	}
	
	public void locationLooper(){
		long endTime = System.currentTimeMillis();
		long startTime = endTime;
		while(startTime-endTime<1000){
			startTime = System.currentTimeMillis();
		}

		
		currentLocation = locationManager.getLastKnownLocation(locationManager.GPS_PROVIDER);
		Log.i(info,"Current location Lat:"+currentLocation.getLatitude()+" Long:"+currentLocation.getLongitude());
		updateGpxFile(currentLocation);
		double [] position = new double[2];
			position = getPositionQueryToServerViaTCP();
		Log.i(info,"Received info: Lat"+ position[0]+"Long:"+position[1]);
		Location newLoc = new Location("From Server");
		newLoc.setLatitude(position[0]);
		newLoc.setLongitude(position[1]);
		lastLocationFromServer = newLoc;
		Log.i(info, ""+position[0]+","+position[1]);
		linearDeadReckoning(lastLocationFromServer);
		locationLooper();
	}

	
	public void onProviderDisabled(String arg0) {
		// TODO Auto-generated method stub
		
	}
	public void onProviderEnabled(String arg0) {
		// TODO Auto-generated method stub
		
	}
	public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
		// TODO Auto-generated method stub
		
	}

	public IBinder onBind(Intent arg0) {
		// TODO Auto-generated method stub
		return iBinder;
	}


		
	/**
     * Function sends location information to the server using UPD protocol
     */
	private void transmitPositionInformationViaUPD(double latitude, double longitude,double ID,double time,double xCoord, double yCoord,double speed){

    		byte[] data; //data to be sent
    		Log.i(info, "transmitPositionInformationViaUPD for Lat:"+latitude+" Long:"+longitude);
    		
    		
//    		if position is sent for the first time via UDP we send ONLY (IN FOLLOWING ORDER!):
//    		-latitude
//    		-longitude
//    		-ID of mobile user
//    		-time
    		if(firstTimeUPD){
    			Log.i(info, "First transmission via UDP");
        		double[] allParam = new double[4];
        		allParam[0]=latitude;
        		allParam[1]=longitude;
        		allParam[2]=ID;
        		allParam[3]=time;
    			firstTimeUPD = false;
    			data = converter.toByta(allParam);
    		} 
    		
//    		else all parameters are sent IN FOLLOWING ORDER!:/    	
//    		-latitude
//    		-longitude
//    		-ID of mobile user
//    		-time
//    		-xCoord of prediction vector
//    		-yCoord of prediction vector
//    		-speed
    		else{
        		double[] allParam = new double[7];
        		allParam[0]=latitude;
        		allParam[1]=longitude;
        		allParam[2]=ID;
        		allParam[3]=time;
        		allParam[4]=xCoord;
        		allParam[5]=yCoord;
        		allParam[6]=speed;
        		data = converter.toByta(allParam);
    		}
    	try {
    		// create socket and bind it
    	
    		if(clientSocket.isBound()){
    			Log.i(info, "clientSocket bound");
    		
    		DatagramPacket packet = new DatagramPacket(data, data.length);
    		// set receiver address
    		packet.setAddress(InetAddress.getByName("10.0.2.2"));
			packet.setPort(6000);
			clientSocket.send(packet);
			updateCount++;
    		}
		
    	} catch (UnknownHostException e) {
			e.printStackTrace();
		}catch (IOException e){
    		e.printStackTrace();
    	}
    }
	
}
